﻿// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition.AttributedModel;
using System.Linq;
using System.Reflection;
using Microsoft.Internal;
using Microsoft.Internal.Collections;
using System.Globalization;

namespace System.ComponentModel.Composition.ReflectionModel
{
    internal abstract class ReflectionComposablePart : ComposablePart
    {
        private readonly Dictionary<ImportingItem, object> _importValues = new Dictionary<ImportingItem, object>();
        private readonly Dictionary<ImportDefinition, ImportingItem> _importsCache = new Dictionary<ImportDefinition, ImportingItem>();
        private readonly Dictionary<ExportDefinition, ExportingMember> _exportsCache = new Dictionary<ExportDefinition, ExportingMember>();
        private bool _importCalledSinceLastImportCompleted = false;
        private bool _compositionCompleted = false;
        private object _cachedInstance;
        
        public ReflectionComposablePart()
        {
            this.IsInstanceLifetimeOwner = true;
        }

        public ReflectionComposablePart(object instance)
        {
            Requires.NotNull(instance, "instance");

            if (instance is ValueType)
            {
                throw new ArgumentException(Strings.ArgumentValueType, "instance");
            }

            this.IsInstanceLifetimeOwner = false;
            this._cachedInstance = instance;
        }

        public override IDictionary<string, object> Metadata
        {
            get
            {
                this.ThrowIfDisposed();
                return this.Definition.Metadata;
            }
        }

        public sealed override IEnumerable<ImportDefinition> ImportDefinitions
        {
            get
            {
                this.ThrowIfDisposed();
                return this.Definition.ImportDefinitions;
            }
        }

        public sealed override IEnumerable<ExportDefinition> ExportDefinitions
        {
            get
            {
                this.ThrowIfDisposed();
                return this.Definition.ExportDefinitions;
            }
        }

        public abstract ReflectionComposablePartDefinition Definition
        {
            get;
        }

        public bool IsInstanceLifetimeOwner 
        { 
            get; 
            set; 
        }

        public override object GetExportedObject(ExportDefinition definition)
        {
            this.ThrowIfDisposed();

            Requires.NotNull(definition, "definition");

            ExportingMember member = GetExportingMemberFromDefinition(definition);
            if (member == null)
            {
                throw ExceptionBuilder.CreateExportDefinitionNotOnThisComposablePart("definition");           
            }

            return GetExportedObject(member);
        }

        public override void SetImport(ImportDefinition definition, IEnumerable<Export> exports)
        {
            this.ThrowIfDisposed();

            Requires.NotNull(definition, "definition");
            Requires.NotNullOrNullElements(exports, "exports");

            ImportingItem item = GetImportingItemFromDefinition(definition);
            if (item == null)
            {
                throw ExceptionBuilder.CreateImportDefinitionNotOnThisComposablePart("definition");
            }

            SetImport(item, exports);
        }

        public override bool Equals(object obj)
        {
            ReflectionComposablePart part = obj as ReflectionComposablePart;

            if (part == null)
            {
                return false;
            }

            // If the instance has not been created yet then the only
            // way they can be the same is if they are binary equivalent
            if (part._cachedInstance == null || this._cachedInstance == null)
            {
                return base.Equals(part);
            }

            // Try to compare parts via their cached instances so that 
            // if the same instance is added more than once (i.e. wrapped 
            // by more than one part) they are equivalent parts.
            return this._cachedInstance.Equals(part._cachedInstance);
        }

        public override int GetHashCode()
        {
            // Should use the hash code of the cached instance but it isn't 
            // always available and we need to make this consistent over the 
            // lifetime of the object so the definition should be a good
            // "bucketing" value.
            return this.Definition.GetHashCode();
        }

        public override void OnComposed()
        {
            this.ThrowIfDisposed();
            var result = CompositionResult.SucceededResult;

            // We may want to actually verify that all the required imports have been set
            // right now that checking won't happen until someone pulls on an export.

            if (this._cachedInstance != null)
            {
                // Only set the non-Prerequsite imports the first time we are composed and have an instance
                if (!this._compositionCompleted)
                {
                    result = result.MergeResult(this.SetPostExportImports());
                    result.ThrowOnErrors();
                }

                // Whenever we are composed/recomposed and have an instance notify the instance
                result = result.MergeResult(this.NotifyImportCompleted());
                result.ThrowOnErrors();
            }

            this._compositionCompleted = true;
        }

        protected override void Dispose(bool disposing)
        {
            try
            {
                if (disposing)
                {
                    if (this.IsInstanceLifetimeOwner)
                    {
                        IDisposable disposable = this._cachedInstance as IDisposable;
                        if (disposable != null)
                        {
                            disposable.Dispose();
                        }
                    }
                }
            }
            finally
            {
                base.Dispose(disposing);
            }
        }

        protected abstract ImportingItem GetImportingItem(ImportDefinition definition);

        protected abstract ExportingMember GetExportingMember(ExportDefinition definition);

        private object GetExportedObject(ExportingMember member)
        {
            object instance = null;
            if (member.RequiresInstance)
            {   // Only activate the instance if we actually need to

                instance = this.GetInstanceActivateIfNeeded().GetValueIfSucceeded();
            }

            return member.GetExportedObject(instance).GetValueIfSucceeded();
        }

        private void SetImport(ImportingItem item, IEnumerable<Export> exports)
        {
            CompositionResult result = CompositionResult.SucceededResult;
            CompositionResult<object> valueResult = item.TryCastExportsToImportType(exports);
            result = result.MergeResult(valueResult.ToResultWithNoValue());
            if (!result.Succeeded)
            {
                throw new CompositionException(result.Issues);
            }

            this._importCalledSinceLastImportCompleted = true;
            this._importValues[item] = valueResult.Value;

            // Go ahead and set it directly if this is a recompose scenario
            if (this._compositionCompleted && this._cachedInstance != null)
            {
                ImportingMember member = item as ImportingMember;
                if (member != null && member.Definition.IsRecomposable)
                {
                    result = result.MergeResult(member.SetValue(this._cachedInstance, valueResult.Value));
                }
            }

            if (!result.Succeeded)
            {
                throw new CompositionException(result.Issues);
            }
        }

        private CompositionResult<object> GetInstanceActivateIfNeeded()
        {
            CompositionResult result = CompositionResult.SucceededResult;

            if (this._cachedInstance != null)
            {
                return result.ToResult<object>(this._cachedInstance);
            }

            CompositionResult<object> instanceResult = this.ConstructObject();

            result = result.MergeIssues(instanceResult.Issues);

            if (!result.Succeeded)
            {
                return result.ToResult<object>(null);
            }

            this._cachedInstance = instanceResult.Value;

            // Right now we only support prerequisite imports as constructor parameters but at
            // some later point we might want to support non-constructor parameters as
            // prerequisite imports and we could potentially accomplish this like below but we
            // would need to figure out what to do if it fails, particularly with _cachedInstance.
            //result = result.MergeResult(SetImportsOnObject(import => import.IsPrerequisite));

            if (this._compositionCompleted)
            {
                result = result.MergeResult(this.SetPostExportImports());
                if (!result.Succeeded)
                {
                    return result.ToResult<object>(null);
                }

                result = result.MergeResult(this.NotifyImportCompleted());
                if (!result.Succeeded)
                {
                    return result.ToResult<object>(null);
                }
            }

            return result.ToResult<object>(this._cachedInstance);
        }

        private CompositionResult<object> ConstructObject()
        {
            var result = CompositionResult.SucceededResult;
            List<object> arguments = new List<object>();
            object instance = null;

            if (this.Definition.Constructor == null)
            {
                var issue = CompositionIssue.CreateIssueAsError(
                    CompositionIssueId.NoSelectedConstructor,
                    Strings.NoSelectedConstructor,
                    this.Definition.PartType.FullName);
                return result.MergeIssue(issue).ToResult<object>(null);
            }

            // We are assuming that these parameters are comming back in the correct
            // order to be passed to the constructor.
            IEnumerable<ImportDefinition> parameters = this.ImportDefinitions.Where(definition => definition.IsPrerequisite);

            result = result.MergeResult(this.UseImportedValues(parameters, (import, value) =>
            {
                arguments.Add(value);
                return CompositionResult.SucceededResult;
            }, true));

            if (!result.Succeeded)
            {
                return result.ToResult<object>(null);
            }

            try
            {
                instance = this.Definition.Constructor.Invoke(arguments.ToArray());
            }
            catch (TargetInvocationException ex)
            {
                var issue = CompositionIssue.CreateIssueAsError(
                    CompositionIssueId.ExceptionDuringComponentActivation,
                    ex.InnerException,
                    Strings.ExceptionDuringComponentActivation,
                    this.Definition.PartType.FullName);

                return result.MergeIssue(issue).ToResult<object>(null);
            }

            return result.ToResult<object>(instance);
        }

        private CompositionResult SetPostExportImports()
        {
            IEnumerable<ImportDefinition> members = this.ImportDefinitions.Where(import => !import.IsPrerequisite);

            return this.UseImportedValues(members, (import, value) =>
                CompositionResult.SucceededResult.MergeResult(((ImportingMember)import).SetValue(this._cachedInstance, value)),
                false); // Dev10:484204 The validation is turned off for post imports because of it broke declarative composition
        }

        private CompositionResult UseImportedValues<T>(IEnumerable<T> definitions, Func<ImportingItem, object, CompositionResult> useImportValue, bool errorIfMissing)
            where T : ImportDefinition
        {
            var result = CompositionResult.SucceededResult;

            foreach (var definition in definitions)
            {
                ImportingItem import = GetImportingItemFromDefinition(definition);

                object importValue = null;

                if (!this._importValues.TryGetValue(import, out importValue))
                {
                    if (!errorIfMissing)
                    {
                        continue;
                    }

                    if (definition.Cardinality == ImportCardinality.ExactlyOne)
                    {
                        var issue = CompositionIssue.CreateIssueAsError(
                            CompositionIssueId.ImportNotSetOnPart,
                            Strings.ImportNotSetOnPart,
                            this.Definition.PartType.FullName,
                            definition.ToString());
                        result = result.MergeIssue(issue);
                        continue;
                    }
                    else
                    {
                        CompositionResult<object> defaultValueResult = import.TryCastExportsToImportType(Enumerable.Empty<Export>());
                        result = result.MergeIssues(defaultValueResult.Issues);

                        if (!defaultValueResult.Succeeded)
                        {
                            continue;
                        }

                        importValue = defaultValueResult.Value;
                    }
                }

                result = result.MergeResult(useImportValue(import, importValue));
            }

            return result;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design",
                 "CA1031:DoNotCatchGeneralExceptionTypes",
                 Justification = "It is important to store exceptions so that notifications can continue to other components")]
        private CompositionResult NotifyImportCompleted()
        {
            var result = CompositionResult.SucceededResult;
            if (this._importCalledSinceLastImportCompleted)
            {
                INotifyImport notify = this._cachedInstance as INotifyImport;
                if (notify != null)
                {
                    // Reentrancy on composition notifications is allowed, so set this first to avoid 
                    // an infinte loop of notifications.
                    this._importCalledSinceLastImportCompleted = false;

                    try
                    {
                        notify.ImportCompleted();
                    }
                    catch (Exception ex)
                    {
                        result = result.MergeIssue(CompositionIssue.CreateIssueAsError(
                            CompositionIssueId.ExceptionDuringImportCompletedCallback,
                            ex,
                            Strings.ExceptionDuringImportCompletedCallback,
                            this._cachedInstance.GetType().FullName));
                    }
                }
            }
            return result;
        }

        private ExportingMember GetExportingMemberFromDefinition(ExportDefinition definition)
        {
            ExportingMember result;
            if (!_exportsCache.TryGetValue(definition, out result))
            {
                result = GetExportingMember(definition);
                if (result != null)
                {
                    _exportsCache[definition] = result;
                }
            }

            return result;
        }

        private ImportingItem GetImportingItemFromDefinition(ImportDefinition definition)
        {
            ImportingItem result;
            if (!_importsCache.TryGetValue(definition, out result))
            {
                result = GetImportingItem(definition);
                if (result != null)
                {
                    _importsCache[definition] = result;
                }
            }

            return result;
        }
    }
}
